/*
 * Copyright (C) 2007  Vianney le Clément
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package fsahoraires.cours;

import static fsahoraires.cours.EmptyHoraire.EMPTY_HORAIRE;
import static fsahoraires.util.Util.loadUrl;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import fsahoraires.cours.Horaire.Jour;

public class CoursFetcher extends Thread {

	private static final Pattern DESCRIPTION_PATTERN = Pattern
			.compile("<!-- Debut texte principal -->\\s*"
					+ "<td[^>]*>\\s*<div[^>]*>(.*)</div>\\s*"
					+ "<div[^>]*>\\s*<p[^>]*>[^<]*</p>\\s*"
					+ "<p[^>]*>\\s*\\[(?:([0-9\\.]+)h)?(?:\\s*\\+\\s*[0-9\\.]+h)?\\]\\s*"
					+ "([0-9\\.]+)\\s*crédits\\s*</p>");

	private final CoursManager manager;
	private final Queue<FetchItem> queue;

	private final List<CoursFetcherListener> listeners = new ArrayList<CoursFetcherListener>();

	public CoursFetcher(CoursManager manager) {
		this.manager = manager;
		this.queue = new LinkedList<FetchItem>();
		this.start();
	}

	private void fireBegin(FetchItem item) {
		try {
			for (CoursFetcherListener listener : listeners)
				listener.begin(item.getCours(), item.fetchDescription,
						item.fetchHoraires);
		} catch (ConcurrentModificationException e) {
			e.printStackTrace();
		}
	}

	private void fireEnd(FetchItem item) {
		try {
			for (CoursFetcherListener listener : listeners)
				listener.end(item.getCours(), item.fetchDescription,
						item.fetchHoraires);
		} catch (ConcurrentModificationException e) {
			e.printStackTrace();
		}
	}

	public void addCoursFetcherListener(CoursFetcherListener listener) {
		listeners.add(listener);
	}

	public void removeCoursFetcherListener(CoursFetcherListener listener) {
		listeners.remove(listener);
	}

	public synchronized void add(Cours cours) {
		add(cours, true, true);
	}

	public synchronized void add(Cours cours, boolean description,
			boolean horaires) {
		FetchItem item = new FetchItem(cours, description, horaires);
		/*
		 * supprimer le cours actuel dans la queue (equals ne regarde que cours)
		 * afin de mettre à jour fetchDescription et fetchHoraires
		 */
		queue.remove(item);
		queue.add(item);
		notifyAll();
	}

	public synchronized void remove(Cours cours) {
		queue.remove(cours);
	}

	private void checkInterrupted() throws InterruptedException {
		if (isInterrupted())
			throw new InterruptedException();
	}

	public void run() {
		try {
			while (!isInterrupted()) {
				FetchItem item;
				synchronized (this) {
					if (queue.isEmpty())
						wait();
					item = queue.remove();
				}
				fireBegin(item);
				Cours cours = item.getCours();
				String titre = "";
				float credits = 0;
				float heures = 0;
				Horaire horaires[] = new Horaire[2];
				if (item.fetchDescription) {
					try {
						String str = loadUrl(cours.getDescription());
						Matcher m = DESCRIPTION_PATTERN.matcher(str);
						if (m.find()) {
							try {
								titre = m.group(1);
								if (m.group(2) == null)
									heures = 0;
								else
									heures = Float.parseFloat(m.group(2));
								credits = Float.parseFloat(m.group(3));
							} catch (NumberFormatException e) {
								item.fetchDescription = false;
							}
						} else {
							item.fetchDescription = false;
						}
					} catch (InterruptedIOException e) {
						throw new InterruptedException();
					} catch (IOException e) {
					}
				}
				if (item.fetchHoraires) {
					checkInterrupted();
					horaires[0] = fetchHoraire(cours.getCode(), 1);
					checkInterrupted();
					horaires[1] = fetchHoraire(cours.getCode(), 2);
				}
				checkInterrupted();
				manager.fireCoursBeforeModify(cours);
				boolean fire = false;
				synchronized (cours) {
					if (cours.isDisposed())
						continue;
					if (item.fetchDescription) {
						if (cours.titre.equals("")) {
							cours.titre = titre;
							fire = true;
						}
						if (cours.credits == 0) {
							cours.credits = credits;
							fire = true;
						}
						if (cours.heures == 0) {
							cours.heures = heures;
							fire = true;
						}
					}
					if (item.fetchHoraires) {
						cours.horaires = horaires;
						fire = true;
					}
				}
				if (fire)
					manager.fireCoursModified(cours);
				fireEnd(item);
			}
		} catch (InterruptedException e) {
		}
	}

	private static final Pattern TABLE_PATTERN = Pattern.compile(
			"<table.*</table>", Pattern.DOTALL);
	private static final Pattern TD_PATTERN = Pattern
			.compile("<td align=left width=160 height=160 valign=top>"
					+ "(?:&nbsp;|<font size=-2>(.*?)</font>)</td>");
	private static final Pattern TEXT_PATTERN = Pattern
			.compile("(.*?)(?:<br>){1,2}");

	private Horaire fetchHoraire(String code, int semestre)
			throws InterruptedException {
		CompoundHoraire horaire = new CompoundHoraire();
		try {
			URL url = new URL(
					"http://perception.fsa.ucl.ac.be/h_affcod.asp?code=" + code
							+ "&V2=" + semestre);
			String str = loadUrl(url);

			Matcher match = TABLE_PATTERN.matcher(str);
			if (!match.find())
				return horaire;

			String table = match.group();
			match = TD_PATTERN.matcher(table);
			int start = 0;
			for (int tranche = 0; tranche < Horaire.TRANCHES; tranche++) {
				for (Jour jour : Jour.values()) {
					if (!match.find(start))
						return EMPTY_HORAIRE;
					start = match.end();
					String td = match.group(1);
					if (td != null) {
						Matcher m = TEXT_PATTERN.matcher(td);
						int s = 0;
						List<Occupation> occupations = new ArrayList<Occupation>();
						while (m.find(s)) {
							s = m.end();
							try {
								occupations.add(new Occupation(m.group(1),
										manager));
							} catch (IllegalArgumentException e) {
								// do nothing
							}
						}
						Occupation[] occ = occupations
								.toArray(new Occupation[occupations.size()]);
						if (Occupation.isCombinable(occ))
							horaire.add(Occupation.combine(occ), jour, tranche);
						else
							for (Occupation o : occupations)
								horaire.add(o, jour, tranche);
					}
				}
			}

		} catch (MalformedURLException e) {
		} catch (InterruptedIOException e) {
			throw new InterruptedException();
		} catch (IOException e) {
		}
		return horaire;
	}

	private class FetchItem {
		private final Cours cours;
		public boolean fetchDescription, fetchHoraires;

		public FetchItem(Cours cours, boolean fetchDescription,
				boolean fetchHoraires) {
			if (cours == null)
				throw new NullPointerException();
			this.cours = cours;
			this.fetchDescription = fetchDescription;
			this.fetchHoraires = fetchHoraires;
		}

		public Cours getCours() {
			return cours;
		}

		public boolean equals(Object obj) {
			if (obj == null)
				return false;
			if (!(obj instanceof FetchItem))
				return false;
			return cours.equals(((FetchItem) obj).cours);
		}
	}

}
